Skip to content

feat: simplify implement of hooks for main-thread runtime#2441

Merged
HuJean merged 1 commit intomainfrom
p/hooks
Apr 15, 2026
Merged

feat: simplify implement of hooks for main-thread runtime#2441
HuJean merged 1 commit intomainfrom
p/hooks

Conversation

@HuJean
Copy link
Copy Markdown
Collaborator

@HuJean HuJean commented Apr 9, 2026

Summary by CodeRabbit

  • New Features

    • Added dedicated hooks entrypoints, including a main-thread–optimized hooks runtime and a centralized internal constants entry.
  • Tests

    • Improved test setup to initialize environment state and added layered alias tests for build-time resolution.
  • Refactor

    • Reorganized public export surface for clearer separation between runtime internals and public entry points.
  • Chores

    • Updated package export/type mappings, added release metadata, and prepared minor package bumps.

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 073ee72

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@lynx-js/react Minor
@lynx-js/react-alias-rsbuild-plugin Minor
@lynx-js/react-rsbuild-plugin Minor
@lynx-js/react-umd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added a Lepus (main-thread) hooks runtime, exposed stable runtime constants and new package export subpaths for hooks/constants, and updated rspeedy aliasing and tests to resolve hook imports differently per execution layer.

Changes

Cohort / File(s) Summary
Release Management
/.changeset/tidy-buttons-tie.md
New changeset declaring minor bumps for @lynx-js/react and @lynx-js/react-alias-rsbuild-plugin.
Package Exports
packages/react/package.json
Added ./internal/constants, ./hooks, and ./lepus/hooks subpath exports and matching typesVersions mappings.
Lepus (main-thread) hooks
packages/react/runtime/lepus/hooks/index.js
New main-thread hook implementation (useState/useReducer/useMemo/useCallback/useRef/useContext/useId/useDebugValue; effect hooks stubbed for background).
Constants surface
packages/react/runtime/src/constants.ts, packages/react/runtime/src/internal.ts
Added src/constants.ts re-exporting opcode constants; removed those opcode re-exports from internal.ts.
Runtime imports / lazy changes
packages/react/runtime/lazy/internal.js, packages/react/runtime/lepus/jsx-runtime/index.js
Stopped exporting vnode opcode symbols from lazy/internal; jsx-runtime now imports constants from @lynx-js/react/internal/constants.
Runtime hooks (profiling/selection)
packages/react/runtime/src/hooks/react.ts
Changed effect/layoutEffect/imprativeHandle selection logic to remove background-noop gating; now chooses based on profiling state and directly re-exports useImperativeHandle from preact/hooks.
Tests & test config
packages/react/runtime/__test__/*, packages/react/runtime/vitest.config.ts
Test setup now switches env to background in beforeEach; Vitest alias added for @lynx-js/react/internal/constants; tests updated for layered alias behavior.
rspeedy alias plugin & tests
packages/rspeedy/plugin-react-alias/src/index.ts, packages/rspeedy/plugin-react-alias/test/index.test.ts
Added layer-aware hook aliasing and internal/constants alias; mapped main-thread to Lepus hook targets and updated/added tests to validate resolver aliases per issuer layer.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • hzy
  • colinaaa
  • luhc228
  • Yradex

Poem

🐇 I hopped through files with tiny, eager paws,
Lepus hooks tucked in, obeying main-thread laws,
Constants bundled cozy in a single nest,
Aliases guide my hopping from test to manifest,
A rabbit dances—release-ready applause.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: simplifying hooks implementation for main-thread runtime, which aligns with the comprehensive changeset including new hooks exports, constants reorganization, and related updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch p/hooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/react/runtime/lepus/hooks/index.js (1)

60-68: useReducer dispatch is a no-op - state updates won't work.

The dispatch function on line 64 is function(action) {}, which silently discards all state updates. If this is intentional for a single-render main-thread context (matching the no-op useEffect/useLayoutEffect), consider adding a brief comment to document this design choice for maintainability.

📝 Suggested documentation
 function useReducer(reducer, initialState, init) {
   var hookState = getHookState(currentIndex++, 2);
   hookState._reducer = reducer;
   if (!hookState[COMPONENT]) {
+    // Dispatch is no-op in main-thread runtime (single render, no state updates)
     hookState[VALUE] = [!init ? invokeOrReturn(undefined, initialState) : init(initialState), function(action) {}];
     hookState[COMPONENT] = currentComponent;
   }
   return hookState[VALUE];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/runtime/lepus/hooks/index.js` around lines 60 - 68,
useReducer's dispatch is currently a no-op (function(action) {}), so state never
updates; replace that no-op with a dispatch that uses hookState._reducer to
compute the next state, assigns it into hookState[VALUE][0], and triggers the
component update via the runtime's render scheduler (use currentComponent or
whatever local scheduler API is available) so the component re-renders with the
new state; if the no-op was intentional, instead add a one-line comment inside
useReducer next to the dispatch (referencing useReducer, hookState,
hookState._reducer, hookState[VALUE], currentComponent) explaining the
single-render/main-thread design decision.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react/runtime/__test__/debug/react-hooks-profile.test.jsx`:
- Line 12: The test environment switch to background mode allows
BackgroundSnapshotInstance state to bleed because
snapshotInstanceManager.clear() is no longer resetting the active tree; update
the environment reset so both backgroundSnapshotInstanceManager and
snapshotInstanceManager are cleared before reinitializing: modify resetEnv() (or
the same routine invoked when switching runtimes, referenced by
globalEnvManager/resetEnv) to call backgroundSnapshotInstanceManager.clear() in
addition to snapshotInstanceManager.clear() so background state is fully reset
between cases.

In `@packages/react/runtime/lepus/hooks/index.js`:
- Around line 118-123: The useId implementation always returns "P0-0" because
mask is a local array recreated on each call; fix by using persistent storage
for the counters (either a module-level counter or store the mask/counter on the
hook state returned by getHookState). Modify function useId so it
reads/increments a persistent counter (e.g., module-scope idCounter or
state.counter on the array returned by getHookState(currentIndex++, 11)) and
then builds state[VALUE] = 'P' + <bucket> + '-' + <incremented counter>; ensure
you reference currentIndex, getHookState, state and VALUE so the counter
survives across calls and produces unique IDs.

In `@packages/rspeedy/plugin-react-alias/test/index.test.ts`:
- Around line 341-357: The test's expectation for backgroundRule.resolve.alias
of 'preact/hooks' is incorrect because the plugin implementation
(plugin-react-alias's resolvePreact('preact/hooks') in
packages/rspeedy/plugin-react-alias/src/index.ts) currently maps 'preact/hooks'
to the preact-specific path, not '/packages/react/runtime/lib/hooks/react.js';
either update the test to expect the path produced by
resolvePreact('preact/hooks') (inspect resolvePreact or
backgroundRule.resolve.alias at runtime) or change the plugin implementation in
plugin-react-alias (the resolvePreact call or alias mapping logic in
src/index.ts) to point 'preact/hooks' to
'/packages/react/runtime/lib/hooks/react.js' so the assertion matches the actual
alias.

---

Nitpick comments:
In `@packages/react/runtime/lepus/hooks/index.js`:
- Around line 60-68: useReducer's dispatch is currently a no-op
(function(action) {}), so state never updates; replace that no-op with a
dispatch that uses hookState._reducer to compute the next state, assigns it into
hookState[VALUE][0], and triggers the component update via the runtime's render
scheduler (use currentComponent or whatever local scheduler API is available) so
the component re-renders with the new state; if the no-op was intentional,
instead add a one-line comment inside useReducer next to the dispatch
(referencing useReducer, hookState, hookState._reducer, hookState[VALUE],
currentComponent) explaining the single-render/main-thread design decision.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: de50334b-3441-43f3-a847-a673c63aae37

📥 Commits

Reviewing files that changed from the base of the PR and between 045ca2f and e7749b9.

📒 Files selected for processing (12)
  • .changeset/tidy-buttons-tie.md
  • packages/react/package.json
  • packages/react/runtime/__test__/debug/react-hooks-profile.test.jsx
  • packages/react/runtime/__test__/hooks/useLynxGlobalEventListener.test.jsx
  • packages/react/runtime/lazy/internal.js
  • packages/react/runtime/lepus/hooks/index.js
  • packages/react/runtime/lepus/jsx-runtime/index.js
  • packages/react/runtime/src/constants.ts
  • packages/react/runtime/src/internal.ts
  • packages/react/runtime/vitest.config.ts
  • packages/rspeedy/plugin-react-alias/src/index.ts
  • packages/rspeedy/plugin-react-alias/test/index.test.ts
💤 Files with no reviewable changes (2)
  • packages/react/runtime/src/internal.ts
  • packages/react/runtime/lazy/internal.js

Comment thread packages/react/runtime/__test__/debug/react-hooks-profile.test.jsx
Comment thread packages/react/runtime/lepus/hooks/index.js Outdated
Comment thread packages/rspeedy/plugin-react-alias/test/index.test.ts
@HuJean HuJean force-pushed the p/hooks branch 2 times, most recently from fd3c796 to 4b259d2 Compare April 9, 2026 12:15
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rspeedy/plugin-react-alias/src/index.ts (1)

131-140: ⚠️ Potential issue | 🟠 Major

Alias @lynx-js/react/lepus/hooks in the background rule as well.

The main-thread alias block (lines 120–128) includes .set('@lynx-js/react/lepus/hooks', reactHooks.mainThread), but the background rule (lines 131–140) omits it. Background code that explicitly imports @lynx-js/react/lepus/hooks will resolve to the main-thread implementation instead of the background version, causing a layer mismatch.

Proposed fix
                .alias
                  .set('react/jsx-runtime', jsxRuntime.background)
                  .set('react/jsx-dev-runtime', jsxDevRuntime.background)
                  .set('@lynx-js/react/jsx-runtime', jsxRuntime.background)
                  .set('@lynx-js/react/jsx-dev-runtime', jsxDevRuntime.background)
                  .set('preact/hooks', reactHooks.preact)
                  .set('@lynx-js/react/hooks', reactHooks.background)
+                 .set('@lynx-js/react/lepus/hooks', reactHooks.background)
                 .end()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rspeedy/plugin-react-alias/src/index.ts` around lines 131 - 140, The
background alias rule (.rule('react:jsx-runtime:background') with
issuerLayer(LAYERS.BACKGROUND)) is missing an alias for
'@lynx-js/react/lepus/hooks', causing imports to resolve to the main-thread
version; add .set('@lynx-js/react/lepus/hooks', reactHooks.background) to the
chain alongside the existing reactHooks mappings so background imports resolve
to the background implementation.
♻️ Duplicate comments (1)
packages/react/runtime/lepus/hooks/index.js (1)

118-122: ⚠️ Potential issue | 🔴 Critical

useId still returns the same ID for every call.

mask is recreated on each invocation, so mask[1]++ always starts from 0 and every component gets P0-0.

Proposed fix
+var idMask = [0, 0];
+
 function useId() {
   var state = getHookState(currentIndex++, 11);
-  var mask = [0, 0];
-  state[VALUE] = 'P' + mask[0] + '-' + mask[1]++;
+  if (state[VALUE] == null) {
+    state[VALUE] = 'P' + idMask[0] + '-' + idMask[1]++;
+  }
   return state[VALUE];
 }
In React and Preact, should `useId` return a stable unique ID per hook call rather than the same ID for every component?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/runtime/lepus/hooks/index.js` around lines 118 - 122, useId
currently recreates mask on every call so every hook returns "P0-0"; fix by
making the ID persistent in the hook state instead of recomputing: inside useId
(which calls getHookState and uses currentIndex and VALUE) only compute and
assign state[VALUE] when it's not already set — e.g. initialize a stable
per-hook counter or store the mask/counter in the returned state array so
subsequent calls reuse it (use getHookState's state slot rather than local var
mask) and increment a module-level counter or the stored counter when generating
new IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react/runtime/lepus/hooks/index.js`:
- Around line 84-87: The current useMemo implementation always calls factory()
and ignores the dependency array (args), causing values to change every render;
modify useMemo to store previous deps in the hook state (e.g., state[ARGS]) and
only call factory() when deps require recomputation: if args is undefined behave
like always-recompute; if args is an array and there are previous deps do a
shallow equality check (compare length and each item via ===) and only run
factory() and update state[VALUE] and state[ARGS] when any item changed;
otherwise return the existing state[VALUE]; implement the shallow compare inline
or as a small helper and keep references to currentIndex, getHookState, VALUE,
and ARGS symbols when updating state.
- Around line 60-67: The dispatched function created in useReducer is a no-op;
replace it with a real dispatcher that captures the hookState and reducer, calls
reducer(currentState, action), compares with Object.is, and if different stores
the new state into hookState[VALUE][0] and triggers a re-render by calling
hookState[COMPONENT].setState({}); ensure you create the dispatcher inside
useReducer so it closes over hookState and reducer (referencing symbols:
useReducer, hookState, VALUE, COMPONENT, reducer, Object.is, invokeOrReturn,
currentComponent, getHookState) and updates state only when Object.is reports a
change.

---

Outside diff comments:
In `@packages/rspeedy/plugin-react-alias/src/index.ts`:
- Around line 131-140: The background alias rule
(.rule('react:jsx-runtime:background') with issuerLayer(LAYERS.BACKGROUND)) is
missing an alias for '@lynx-js/react/lepus/hooks', causing imports to resolve to
the main-thread version; add .set('@lynx-js/react/lepus/hooks',
reactHooks.background) to the chain alongside the existing reactHooks mappings
so background imports resolve to the background implementation.

---

Duplicate comments:
In `@packages/react/runtime/lepus/hooks/index.js`:
- Around line 118-122: useId currently recreates mask on every call so every
hook returns "P0-0"; fix by making the ID persistent in the hook state instead
of recomputing: inside useId (which calls getHookState and uses currentIndex and
VALUE) only compute and assign state[VALUE] when it's not already set — e.g.
initialize a stable per-hook counter or store the mask/counter in the returned
state array so subsequent calls reuse it (use getHookState's state slot rather
than local var mask) and increment a module-level counter or the stored counter
when generating new IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eb5652ea-331f-444b-8f69-3d715bd8f658

📥 Commits

Reviewing files that changed from the base of the PR and between fd3c796 and 4b259d2.

📒 Files selected for processing (12)
  • .changeset/tidy-buttons-tie.md
  • packages/react/package.json
  • packages/react/runtime/__test__/debug/react-hooks-profile.test.jsx
  • packages/react/runtime/__test__/hooks/useLynxGlobalEventListener.test.jsx
  • packages/react/runtime/lazy/internal.js
  • packages/react/runtime/lepus/hooks/index.js
  • packages/react/runtime/lepus/jsx-runtime/index.js
  • packages/react/runtime/src/constants.ts
  • packages/react/runtime/src/internal.ts
  • packages/react/runtime/vitest.config.ts
  • packages/rspeedy/plugin-react-alias/src/index.ts
  • packages/rspeedy/plugin-react-alias/test/index.test.ts
💤 Files with no reviewable changes (2)
  • packages/react/runtime/src/internal.ts
  • packages/react/runtime/lazy/internal.js
✅ Files skipped from review due to trivial changes (5)
  • packages/react/runtime/test/hooks/useLynxGlobalEventListener.test.jsx
  • .changeset/tidy-buttons-tie.md
  • packages/react/runtime/lepus/jsx-runtime/index.js
  • packages/react/runtime/src/constants.ts
  • packages/react/runtime/vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/react/runtime/test/debug/react-hooks-profile.test.jsx
  • packages/rspeedy/plugin-react-alias/test/index.test.ts
  • packages/react/package.json

Comment thread packages/react/runtime/lepus/hooks/index.js Outdated
Comment thread packages/react/runtime/lepus/hooks/index.js Outdated
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 9, 2026

Merging this PR will degrade performance by 15.45%

⚡ 4 improved benchmarks
❌ 2 regressed benchmarks
✅ 75 untouched benchmarks
⏩ 21 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
basic-performance-div-1000 35.3 ms 26.1 ms +35.18%
transform 1000 view elements 42.7 ms 45.4 ms -6%
008-many-use-state__main-thread-serializeRoot 159.2 µs 84.3 µs +88.93%
008-many-use-state__main-thread-renderMainThread 79.9 ms 68.5 ms +16.67%
008-many-use-state-destroyBackground 8 ms 9.5 ms -15.45%
basic-performance-text-200 20.7 ms 11.7 ms +76.67%

Comparing p/hooks (073ee72) with main (d1c16c5)

Open in CodSpeed

Footnotes

  1. 21 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 9, 2026

React External

#424 Bundle Size — 580.35KiB (-0.42%).

073ee72(current) vs d1c16c5 main#418(baseline)

Bundle metrics  Change 1 change
                 Current
#424
     Baseline
#418
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 30.01% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Improvement 1 improvement
                 Current
#424
     Baseline
#418
Improvement  Other 580.35KiB (-0.42%) 582.81KiB

Bundle analysis reportBranch p/hooksProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 9, 2026

Web Explorer

#8881 Bundle Size — 749.3KiB (0%).

073ee72(current) vs d1c16c5 main#8874(baseline)

Bundle metrics  Change 1 change
                 Current
#8881
     Baseline
#8874
No change  Initial JS 44.45KiB 44.45KiB
No change  Initial CSS 2.16KiB 2.16KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 8 8
No change  Assets 10 10
Change  Modules 148(-0.67%) 149
No change  Duplicate Modules 11 11
No change  Duplicate Code 35.01% 35.01%
No change  Packages 3 3
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#8881
     Baseline
#8874
No change  Other 401.63KiB 401.63KiB
No change  JS 345.51KiB 345.51KiB
No change  CSS 2.16KiB 2.16KiB

Bundle analysis reportBranch p/hooksProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 9, 2026

React MTF Example

#440 Bundle Size — 191.12KiB (-0.9%).

073ee72(current) vs d1c16c5 main#433(baseline)

Bundle metrics  Change 3 changes Improvement 1 improvement
                 Current
#440
     Baseline
#433
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 42.32% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 173 173
Improvement  Duplicate Modules 66(-1.49%) 67
Change  Duplicate Code 43.94%(-3.13%) 45.36%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Improvement 1 improvement
                 Current
#440
     Baseline
#433
No change  IMG 111.23KiB 111.23KiB
Improvement  Other 79.89KiB (-2.12%) 81.61KiB

Bundle analysis reportBranch p/hooksProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 9, 2026

React Example

#7307 Bundle Size — 220.51KiB (-1.37%).

073ee72(current) vs d1c16c5 main#7300(baseline)

Bundle metrics  Change 3 changes Improvement 1 improvement
                 Current
#7307
     Baseline
#7300
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 34.81% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 179 179
Improvement  Duplicate Modules 69(-1.43%) 70
Change  Duplicate Code 44.48%(-2.8%) 45.76%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Improvement 1 improvement
                 Current
#7307
     Baseline
#7300
No change  IMG 145.76KiB 145.76KiB
Improvement  Other 74.75KiB (-3.94%) 77.82KiB

Bundle analysis reportBranch p/hooksProject dashboard


Generated by RelativeCIDocumentationReport issue

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
packages/react/runtime/lepus/hooks/index.js (3)

60-67: ⚠️ Potential issue | 🔴 Critical

useReducer dispatch is still a no-op, so state updates are dropped.

On Line 64, the dispatcher never calls reducer, never writes next state, and never schedules a render. This breaks both useReducer and useState update flow.

🐛 Proposed fix
 function useReducer(reducer, initialState, init) {
   var hookState = getHookState(currentIndex++, 2);
   hookState._reducer = reducer;
   if (!hookState[COMPONENT]) {
-    hookState[VALUE] = [!init ? invokeOrReturn(undefined, initialState) : init(initialState), function(action) {}];
+    hookState[VALUE] = [!init ? invokeOrReturn(undefined, initialState) : init(initialState), function(action) {
+      var currentValue = hookState[VALUE][0];
+      var nextValue = hookState._reducer(currentValue, action);
+      if (!Object.is(currentValue, nextValue)) {
+        hookState[VALUE][0] = nextValue;
+        hookState[COMPONENT].setState({});
+      }
+    }];
     hookState[COMPONENT] = currentComponent;
   }
   return hookState[VALUE];
 }
In Preact hooks, what does the `dispatch` function returned by `useReducer` do when next state differs from current state?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/runtime/lepus/hooks/index.js` around lines 60 - 67, The
dispatch returned from useReducer is a no-op; change the inline dispatch (the
second element of hookState[VALUE]) to accept an action, obtain the current
state from hookState[VALUE][0], compute nextState by calling
hookState._reducer(currentState, action), compare nextState to currentState
(strict equality), and if different assign hookState[VALUE][0] = nextState and
schedule a component update (call the framework's render/enqueue update helper
for currentComponent) so the component re-renders; ensure you reference
hookState._reducer, hookState[VALUE], and currentComponent when implementing
this.

84-87: ⚠️ Potential issue | 🔴 Critical

useMemo ignores dependencies and recomputes every render.

Lines 84-87 always call factory(). This breaks memoization and invalidates useCallback/useRef stability guarantees.

🐛 Proposed fix
 function useMemo(factory, args) {
   var state = getHookState(currentIndex++, 7);
-  state[VALUE] = factory();
+  if (!args || !state._args || argsChanged(state._args, args)) {
+    state[VALUE] = factory();
+    state._args = args;
+  }
   return state[VALUE];
 }
+
+function argsChanged(oldArgs, newArgs) {
+  if (oldArgs.length !== newArgs.length) return true;
+  for (var i = 0; i < oldArgs.length; i++) {
+    if (!Object.is(oldArgs[i], newArgs[i])) return true;
+  }
+  return false;
+}
In current Preact hooks, does `useMemo` reuse cached value when dependency values are unchanged, and are `useCallback`/`useRef` implemented on top of that behavior?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/runtime/lepus/hooks/index.js` around lines 84 - 87, useMemo
currently always calls factory() and ignores dependencies; change useMemo (the
function using getHookState and currentIndex) to read the previous deps from the
hook state, perform a shallow equality check between prevDeps and the incoming
args, and only call factory() and update state[VALUE] when args are provided and
differ (if args is undefined, preserve existing React/Preact semantics and
recompute every render). Store the new deps into the hook state (e.g.,
state[DEPS] or state[1]) after computing so subsequent renders can compare; this
will restore memoization used by useCallback/useRef which depend on stable
values from useMemo. Ensure the comparison checks length and each element with
=== (shallow).

118-123: ⚠️ Potential issue | 🔴 Critical

useId currently generates the same ID (P0-0) for all calls.

Line 120 recreates mask on each invocation, so Line 121 never advances globally and IDs are not unique.

🐛 Proposed fix
+var idMask = [0, 0];
+
 function useId() {
   var state = getHookState(currentIndex++, 11);
-  var mask = [0, 0];
-  state[VALUE] = 'P' + mask[0] + '-' + mask[1]++;
+  if (!state[VALUE]) {
+    state[VALUE] = 'P' + idMask[0] + '-' + idMask[1]++;
+  }
   return state[VALUE];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/runtime/lepus/hooks/index.js` around lines 118 - 123, useId
currently creates a new local mask on each call so IDs never advance; persist
the counters instead of recreating mask each invocation by storing the
counter(s) in the hook state returned by getHookState (or in a module-level
counter) and update them when generating the ID. Specifically, in useId, read or
initialize a persistent counter from state (e.g., state.counter or state[VALUE]
slot), compute the ID using that counter (replacing the local mask),
increment/rollover the counters appropriately, assign the generated ID back to
state[VALUE], and return it so subsequent calls produce unique IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/react/runtime/lepus/hooks/index.js`:
- Around line 60-67: The dispatch returned from useReducer is a no-op; change
the inline dispatch (the second element of hookState[VALUE]) to accept an
action, obtain the current state from hookState[VALUE][0], compute nextState by
calling hookState._reducer(currentState, action), compare nextState to
currentState (strict equality), and if different assign hookState[VALUE][0] =
nextState and schedule a component update (call the framework's render/enqueue
update helper for currentComponent) so the component re-renders; ensure you
reference hookState._reducer, hookState[VALUE], and currentComponent when
implementing this.
- Around line 84-87: useMemo currently always calls factory() and ignores
dependencies; change useMemo (the function using getHookState and currentIndex)
to read the previous deps from the hook state, perform a shallow equality check
between prevDeps and the incoming args, and only call factory() and update
state[VALUE] when args are provided and differ (if args is undefined, preserve
existing React/Preact semantics and recompute every render). Store the new deps
into the hook state (e.g., state[DEPS] or state[1]) after computing so
subsequent renders can compare; this will restore memoization used by
useCallback/useRef which depend on stable values from useMemo. Ensure the
comparison checks length and each element with === (shallow).
- Around line 118-123: useId currently creates a new local mask on each call so
IDs never advance; persist the counters instead of recreating mask each
invocation by storing the counter(s) in the hook state returned by getHookState
(or in a module-level counter) and update them when generating the ID.
Specifically, in useId, read or initialize a persistent counter from state
(e.g., state.counter or state[VALUE] slot), compute the ID using that counter
(replacing the local mask), increment/rollover the counters appropriately,
assign the generated ID back to state[VALUE], and return it so subsequent calls
produce unique IDs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 325964a9-4c9f-41c6-b01f-fc1d2ca2aeae

📥 Commits

Reviewing files that changed from the base of the PR and between 4b259d2 and b287f66.

📒 Files selected for processing (12)
  • .changeset/tidy-buttons-tie.md
  • packages/react/package.json
  • packages/react/runtime/__test__/debug/react-hooks-profile.test.jsx
  • packages/react/runtime/__test__/hooks/useLynxGlobalEventListener.test.jsx
  • packages/react/runtime/lazy/internal.js
  • packages/react/runtime/lepus/hooks/index.js
  • packages/react/runtime/lepus/jsx-runtime/index.js
  • packages/react/runtime/src/constants.ts
  • packages/react/runtime/src/internal.ts
  • packages/react/runtime/vitest.config.ts
  • packages/rspeedy/plugin-react-alias/src/index.ts
  • packages/rspeedy/plugin-react-alias/test/index.test.ts
💤 Files with no reviewable changes (2)
  • packages/react/runtime/src/internal.ts
  • packages/react/runtime/lazy/internal.js
✅ Files skipped from review due to trivial changes (5)
  • packages/react/runtime/test/debug/react-hooks-profile.test.jsx
  • .changeset/tidy-buttons-tie.md
  • packages/react/runtime/vitest.config.ts
  • packages/react/runtime/test/hooks/useLynxGlobalEventListener.test.jsx
  • packages/react/runtime/lepus/jsx-runtime/index.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/react/runtime/src/constants.ts
  • packages/react/package.json
  • packages/rspeedy/plugin-react-alias/src/index.ts

@HuJean HuJean changed the title feat: simplify hooks for main-thread runtime feat: simplify implement of hooks for main-thread runtime Apr 11, 2026
@HuJean HuJean force-pushed the p/hooks branch 9 times, most recently from d04260b to e18cc33 Compare April 14, 2026 09:07
Copy link
Copy Markdown
Collaborator

@Yradex Yradex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useId() currently recreates mask = [0, 0] on every call, so it always returns P0-0.

That means multiple useId() calls in the same tree will collide, which breaks any logic relying on stable unique IDs. The new test only covers a single call, so this can still pass with green CI.

Could we make the generated id persistent across hook calls instead of rebuilding the counter each time?

@HuJean HuJean force-pushed the p/hooks branch 2 times, most recently from fb3eebf to b0c07e5 Compare April 14, 2026 11:38
Yradex
Yradex previously approved these changes Apr 14, 2026
Copy link
Copy Markdown
Collaborator

@Yradex Yradex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up commits addressed the useId issue I called out earlier, and I don't see any remaining blocking problems in the latest revision.

Comment thread packages/react/runtime/src/hooks/mainThread.ts
Comment thread packages/react/runtime/src/hooks/react.ts
@HuJean HuJean merged commit f542d9c into main Apr 15, 2026
48 of 50 checks passed
@HuJean HuJean deleted the p/hooks branch April 15, 2026 07:27
colinaaa pushed a commit that referenced this pull request Apr 19, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @lynx-js/react@0.119.0

### Minor Changes

- Simplify hooks for main-thread runtime, which only can run during the
first screen.
([#2441](#2441))

### Patch Changes

- Remove stale gestures when gestures are removed
([#2297](#2297))

- Trace refactor
([#2466](#2466))

    -   Remove `ReactLynx::renderOpcodes` from the trace
- Use `ReactLynx::transferRoot` to measure the time spent transferring
the root to the background thread

- refactor: set state of suspense to render fallback
([#2450](#2450))

- Support rstest for testing library, you can use rstest with RLTL now:
([#2328](#2328))

    Create a config file `rstest.config.ts` with the following content:

    ```ts
    import { defineConfig } from "@rstest/core";
import { withLynxConfig } from
"@lynx-js/react/testing-library/rstest-config";

    export default defineConfig({
      extends: withLynxConfig(),
    });
    ```

`@lynx-js/react/testing-library/rstest-config` will automatically load
your `lynx.config.ts` and apply the same configuration to rstest, so you
can keep your test environment consistent with your development
environment.

    And then use rstest as usual:

    ```bash
    $ rstest
    ```

    For more usage detail, see <https://rstest.rs/>

- Update preact version
([#2456](#2456))

- Add `nodeIndex` to generated FiberElement creation calls and expose
React transform debug metadata as `uiSourceMapRecords`.
([#2402](#2402))

## @lynx-js/react-rsbuild-plugin@0.16.0

### Minor Changes

- Simplify hooks for main-thread runtime, which only can run during the
first screen.
([#2441](#2441))

### Patch Changes

- Support rstest for testing library using a dedicated testing loader.
([#2328](#2328))

- Fix `environments.lynx.performance.profile` so it overrides the
default profile behavior for the current environment.
([#2468](#2468))

- Add `enableUiSourceMap` option to enable UI source map generation and
debug-metadata asset emission.
([#2402](#2402))

- Updated dependencies
\[[`a9f8d05`](a9f8d05),
[`b1ad1b9`](b1ad1b9),
[`f6184f3`](f6184f3),
[`f6184f3`](f6184f3),
[`a9f8d05`](a9f8d05),
[`f542d9c`](f542d9c)]:
    -   @lynx-js/template-webpack-plugin@0.10.9
    -   @lynx-js/react-webpack-plugin@0.9.1
    -   @lynx-js/react-alias-rsbuild-plugin@0.16.0
    -   @lynx-js/css-extract-webpack-plugin@0.7.0
    -   @lynx-js/react-refresh-webpack-plugin@0.3.5
    -   @lynx-js/use-sync-external-store@1.5.0

## @lynx-js/react-alias-rsbuild-plugin@0.16.0

### Minor Changes

- Simplify hooks for main-thread runtime, which only can run during the
first screen.
([#2441](#2441))

### Patch Changes

- fix(rstest): add global fallback aliases for
`@lynx-js/react/jsx-runtime` and `@lynx-js/react/jsx-dev-runtime`
([#2464](#2464))

`pluginReactAlias` only aliased these entries inside layer-specific
rules (`issuerLayer: BACKGROUND/MAIN_THREAD`). In rstest mode there are
no layers, so JSX transformed by the testing loader—which emits `import
{ jsx } from '@lynx-js/react/jsx-runtime'`—could not be resolved,
causing a `Cannot find module '@lynx-js/react/jsx-runtime'` error. Added
global (non-layer-specific) fallback aliases pointing to the background
jsx-runtime.

## @lynx-js/testing-environment@0.2.0

### Minor Changes

- **BREAKING CHANGE**:
([#2328](#2328))

    Align the public test-environment API around `LynxEnv`.

`LynxTestingEnv` now expects a `{ window }`-shaped environment instead
of relying on a concrete `JSDOM` instance or `global.jsdom`. Callers
that construct `LynxTestingEnv` manually or initialize the environment
through globals should migrate to `new LynxTestingEnv({ window })` or
set `global.lynxEnv`.

This release also adds the `@lynx-js/testing-environment/env/rstest`
entry for running the shared testing-environment suite under rstest.

### Patch Changes

- Add `__RemoveGestureDetector` PAPI binding
([#2297](#2297))

## @lynx-js/rspeedy@0.14.2

### Patch Changes

-   Updated dependencies \[]:
    -   @lynx-js/web-rsbuild-server-middleware@0.20.2

## create-rspeedy@0.14.2

### Patch Changes

- Add Rstest ReactLynx Testing Library template.
([#2328](#2328))

## @lynx-js/external-bundle-rsbuild-plugin@0.1.1

### Patch Changes

- Updated dependencies
\[[`3262ca8`](3262ca8)]:
    -   @lynx-js/externals-loading-webpack-plugin@0.1.1

## @lynx-js/web-core@0.20.2

### Patch Changes

- fix: map clientX and clientY to x and y in touch event detail
([#2458](#2458))

- fix(web-platform): completely detach event listeners and forcefully
free `MainThreadWasmContext` pointer alongside strict FIFO async
component disposal to ensure total memory reclamation without
use-after-free risks
([#2457](#2457))

- refactor: with WeakRef in element APIs and WASM bindings to improve
memory management.
([#2439](#2439))

- fix: preserve CSS variable fallback values when encoding web-core
stylesheets so declarations like `var(--token, rgba(...))` are emitted
with their fallback intact.
([#2460](#2460))

- fix: avoid to do use-after-free for rust instance
([#2461](#2461))

- fix: Change uniqueId to uid in LynxCrossThreadEventTarget
([#2467](#2467))

-   Updated dependencies \[]:
    -   @lynx-js/web-worker-rpc@0.20.2

## @lynx-js/externals-loading-webpack-plugin@0.1.1

### Patch Changes

- fix: deduplicate `loadScript` calls for externals sharing the same
(bundle, section) pair
([#2465](#2465))

When multiple externals had different `libraryName` values but pointed
to the same
bundle URL and section path,
`createLoadExternalSync`/`createLoadExternalAsync` was
called once per external, causing `lynx.loadScript` to execute
redundantly for the
same section. Now only the first external in each `(url, sectionPath)`
group triggers
the load; subsequent externals in the group are assigned the
already-loaded result
    directly.

## @lynx-js/react-webpack-plugin@0.9.1

### Patch Changes

- Support rstest for testing library using a dedicated testing loader.
([#2328](#2328))

- fix(rstest): normalize partial `compat` options in the testing loader
([#2464](#2464))

The testing loader forwards `compat` directly to
`transformReactLynxSync` without normalization. When `compat` is
supplied as a partial object, the required `target` field is absent and
the WASM transform throws `Error: Missing field 'target'`. Added the
same normalization already present in `getCommonOptions` for the
background/main-thread loaders: fills in `target: 'MIXED'` and all other
required fields with sensible defaults.

- Add `enableUiSourceMap` option to enable UI source map generation and
debug-metadata asset emission.
([#2402](#2402))

## @lynx-js/template-webpack-plugin@0.10.9

### Patch Changes

- Introduce `LynxDebugMetadataPlugin` to emit debug-metadata assets.
([#2402](#2402))

- Updated dependencies
\[[`24c4c69`](24c4c69),
[`7332eb4`](7332eb4),
[`fd0cc6e`](fd0cc6e),
[`e5b0f66`](e5b0f66),
[`5aa97d8`](5aa97d8),
[`5c39654`](5c39654)]:
    -   @lynx-js/web-core@0.20.2

## @lynx-js/react-umd@0.119.0



## upgrade-rspeedy@0.14.2



## @lynx-js/web-rsbuild-server-middleware@0.20.2



## @lynx-js/web-worker-rpc@0.20.2

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants